Исследование результатов А/А/В - эксперимента для мобильного приложения¶

Вводная часть¶

Цель исследования¶

По результатам А/А/В - эксперимента дать оценку проведенным визуальным изменениям мобильного приложения

Вопросы для исследования¶

  1. Изучить предложенный датафрейм с представленным логом. Провести при необходимости предобработку
  2. Изучить воронку продаж:
  • определить "путь пользователя" от посещения стартовой страницы до покупки,
  • проанализировать "моменты застревания",
  • выявить возможные причины таких моментов.
  1. Проанализировать А/А/В - эксперимент.

Описание данных¶

Дизайнеры предложил провести замену шрифтов в мобильном приложении стартапа, который продает продукты питания.

Отдел продаж сомневается в необходимости предложенных изменений.

По итогу, проведён A/A/B-эксперимент. Пользователи приложения были разбиты на 3 группы: 2 контрольные (246, 247) со старыми шрифтами и одну экспериментальную (248) — с новыми.

По итогам проведенного тестирования был загружен лог с данными

Каждая запись в логе — это действие пользователя, или событие.

  • EventName — название события;
  • DeviceIDHash — уникальный идентификатор пользователя;
  • EventTimestamp — время события;
  • ExpId — номер эксперимента: 246 и 247 — контрольные группы, а 248 — экспериментальная.

Загрузим необходимые для работы библиотеки

In [1]:
import pandas as pd
import datetime as dt
import numpy as np
import matplotlib.pyplot as plt
from pandas.plotting import register_matplotlib_converters
import warnings
import scipy.stats as stats
from scipy import stats as st
import seaborn as sns
import plotly.express as px
from plotly import graph_objects as go
import math as mth
pd.options.mode.chained_assignment = None

Обработка данных¶

Предобработка датафрейма¶

Загрузим файл с данными, затем выгрузим получившуюся таблицу, а так же информацию об ее составе

In [2]:
data=pd.read_csv('/Users/User/Downloads/logs_exp.csv', delim_whitespace=True )
data.head(10)
Out[2]:
EventName DeviceIDHash EventTimestamp ExpId
0 MainScreenAppear 4575588528974610257 1564029816 246
1 MainScreenAppear 7416695313311560658 1564053102 246
2 PaymentScreenSuccessful 3518123091307005509 1564054127 248
3 CartScreenAppear 3518123091307005509 1564054127 248
4 PaymentScreenSuccessful 6217807653094995999 1564055322 248
5 CartScreenAppear 6217807653094995999 1564055323 248
6 OffersScreenAppear 8351860793733343758 1564066242 246
7 MainScreenAppear 5682100281902512875 1564085677 246
8 MainScreenAppear 1850981295691852772 1564086702 247
9 MainScreenAppear 5407636962369102641 1564112112 246
  1. Приведем название столбцов к классическому виду
  2. Добавим два столбца date_time и date, с более привычным видом дат
In [3]:
data= data.rename(columns={'EventName':'event_name', 'DeviceIDHash':'user_id', 
                                   'EventTimestamp':'event_time_stamp', 'ExpId':'group' })
data['date_time'] = pd.to_datetime(data['event_time_stamp'], unit='s')
data['date'] = data['date_time'].dt.strftime('%Y-%m-%d')
data.head(10)
Out[3]:
event_name user_id event_time_stamp group date_time date
0 MainScreenAppear 4575588528974610257 1564029816 246 2019-07-25 04:43:36 2019-07-25
1 MainScreenAppear 7416695313311560658 1564053102 246 2019-07-25 11:11:42 2019-07-25
2 PaymentScreenSuccessful 3518123091307005509 1564054127 248 2019-07-25 11:28:47 2019-07-25
3 CartScreenAppear 3518123091307005509 1564054127 248 2019-07-25 11:28:47 2019-07-25
4 PaymentScreenSuccessful 6217807653094995999 1564055322 248 2019-07-25 11:48:42 2019-07-25
5 CartScreenAppear 6217807653094995999 1564055323 248 2019-07-25 11:48:43 2019-07-25
6 OffersScreenAppear 8351860793733343758 1564066242 246 2019-07-25 14:50:42 2019-07-25
7 MainScreenAppear 5682100281902512875 1564085677 246 2019-07-25 20:14:37 2019-07-25
8 MainScreenAppear 1850981295691852772 1564086702 247 2019-07-25 20:31:42 2019-07-25
9 MainScreenAppear 5407636962369102641 1564112112 246 2019-07-26 03:35:12 2019-07-26
In [4]:
data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 244126 entries, 0 to 244125
Data columns (total 6 columns):
 #   Column            Non-Null Count   Dtype         
---  ------            --------------   -----         
 0   event_name        244126 non-null  object        
 1   user_id           244126 non-null  int64         
 2   event_time_stamp  244126 non-null  int64         
 3   group             244126 non-null  int64         
 4   date_time         244126 non-null  datetime64[ns]
 5   date              244126 non-null  object        
dtypes: datetime64[ns](1), int64(3), object(2)
memory usage: 11.2+ MB
  • найдем дубликаты записей в логах, удалим их
  • найдем пустые значения
In [5]:
print('В датафрейме', data.duplicated().sum(), 'дубликатов')
В датафрейме 413 дубликатов
In [6]:
data = data.drop_duplicates().reset_index(drop=True)
data.isna().sum()
Out[6]:
event_name          0
user_id             0
event_time_stamp    0
group               0
date_time           0
date                0
dtype: int64
In [7]:
data.head(10)
Out[7]:
event_name user_id event_time_stamp group date_time date
0 MainScreenAppear 4575588528974610257 1564029816 246 2019-07-25 04:43:36 2019-07-25
1 MainScreenAppear 7416695313311560658 1564053102 246 2019-07-25 11:11:42 2019-07-25
2 PaymentScreenSuccessful 3518123091307005509 1564054127 248 2019-07-25 11:28:47 2019-07-25
3 CartScreenAppear 3518123091307005509 1564054127 248 2019-07-25 11:28:47 2019-07-25
4 PaymentScreenSuccessful 6217807653094995999 1564055322 248 2019-07-25 11:48:42 2019-07-25
5 CartScreenAppear 6217807653094995999 1564055323 248 2019-07-25 11:48:43 2019-07-25
6 OffersScreenAppear 8351860793733343758 1564066242 246 2019-07-25 14:50:42 2019-07-25
7 MainScreenAppear 5682100281902512875 1564085677 246 2019-07-25 20:14:37 2019-07-25
8 MainScreenAppear 1850981295691852772 1564086702 247 2019-07-25 20:31:42 2019-07-25
9 MainScreenAppear 5407636962369102641 1564112112 246 2019-07-26 03:35:12 2019-07-26

Проверим на возможность попадания одного и того же пользователя в разные группы. Это необходимо для того, чтобы результаты A/B теста были корректны.

In [8]:
res=[]
for k in (data['group'].unique()):
    list1= data.loc[data["group"]==k, "user_id"].unique() 
    for t in (data['group'].unique()):
            list2=data.loc[data["group"]==t, "user_id"].unique()
            if k!=t:
                res=set(list1).intersection(list2)
print(res)
set()

Так как список set вывелся пустым, то можем сделать вывод, что персечений пользователей между группами нет

Промежуточный вывод

  • загружен датафрейм с информацией по проведенному эксперименту
  • проведена предобработка данных:
    • столбцы переименованы в более удобные для работы;
    • добавлены столбцы с датой си временем события ['date_time'], и датой события ['date']
    • найдены и удалены дубликаты;
    • датафрейм проверен на наличие пропущенных значений,
    • датафрейм проверен на пересечение полльзователей между группами - таких пользователей не обнаружено Датафрейм подготовлен для дальнейшей работы.

Анализ датафрема¶

Изучим представленную информацию

In [9]:
print('Всего в логе отображено', data['event_time_stamp'].count(), 'событий')
print('В эксперименте приняло участие', data['user_id'].nunique(), 'пользователь')
print('Данные отражены за период с', min(data['date']), 'по', max(data['date']))
print('В среднем на одного пользователя приходится', round(data['event_time_stamp'].count()/data['user_id'].nunique(), 2), 
     'события. Рассмотрим, как распределены пользователи в разрезе событий (столбец "event_name") ')
Всего в логе отображено 243713 событий
В эксперименте приняло участие 7551 пользователь
Данные отражены за период с 2019-07-25 по 2019-08-07
В среднем на одного пользователя приходится 32.28 события. Рассмотрим, как распределены пользователи в разрезе событий (столбец "event_name") 

Проанализируем, как меняется количество данных. Для этого пострим диаграмму в разрезе событий - это даст нам понимание, насколько полные данные есть по датам

In [10]:
plt.figure(figsize=(20, 10))
ax = sns.countplot(y=data['date'], hue='event_name', data=data)
ax.set_title('Количество различных событий в зависимости от времени в разрезе событий')
plt.show()

На графике видно, что часть данных в логах представленно неполно. Это связано с тем, что по некоторым пользователям события могут "доезжать" позже.

При проведении анализа, такие полупустые дни могут существенно искажать получаемую информацию по эксперименту, поэтому сделаем срез, изменив временной интервал, а так же рассчитаем, какое количество событий мы удалили из выборки, и как это может повлиять на проводимый анализ

  • сделаем срез по событиям с 2019-08-01,
  • определим, какое количество событий мы удалим из выборки.
In [11]:
data_loc = data.query('date >= "2019-08-01"')
print('Всего в логе отображено', data_loc['event_time_stamp'].count(), 'событий')
print('В эксперименте приняло участие', data_loc['user_id'].nunique(), 'пользователь')
print('Новые данные отражены за период с', min(data_loc['date']), 'по', max(data_loc['date']))
print('В среднем на одного пользователя приходится', round(data_loc['event_time_stamp'].count()/data_loc['user_id'].nunique(), 2), 
     'события.')
print('Было убрано', data['event_time_stamp'].count()-data_loc['event_time_stamp'].count(), 
      'событий, прошедших ранее 01.08.2019б что составляет', 
      round((data['event_time_stamp'].count()-data_loc['event_time_stamp'].count())/data['event_time_stamp'].count()*100,2), 
      '% от первоначального количества.' )
print('Были удалены', data['user_id'].nunique()-data_loc['user_id'].nunique(), 
      'пользователей, прошедших ранее 01.08.2019б что составляет', 
      round((data['user_id'].nunique()-data_loc['user_id'].nunique())/data['user_id'].nunique()*100,2), 
      '% от первоначального количества.' )
data_loc
Всего в логе отображено 240887 событий
В эксперименте приняло участие 7534 пользователь
Новые данные отражены за период с 2019-08-01 по 2019-08-07
В среднем на одного пользователя приходится 31.97 события.
Было убрано 2826 событий, прошедших ранее 01.08.2019б что составляет 1.16 % от первоначального количества.
Были удалены 17 пользователей, прошедших ранее 01.08.2019б что составляет 0.23 % от первоначального количества.
Out[11]:
event_name user_id event_time_stamp group date_time date
2826 Tutorial 3737462046622621720 1564618048 246 2019-08-01 00:07:28 2019-08-01
2827 MainScreenAppear 3737462046622621720 1564618080 246 2019-08-01 00:08:00 2019-08-01
2828 MainScreenAppear 3737462046622621720 1564618135 246 2019-08-01 00:08:55 2019-08-01
2829 OffersScreenAppear 3737462046622621720 1564618138 246 2019-08-01 00:08:58 2019-08-01
2830 MainScreenAppear 1433840883824088890 1564618139 247 2019-08-01 00:08:59 2019-08-01
... ... ... ... ... ... ...
243708 MainScreenAppear 4599628364049201812 1565212345 247 2019-08-07 21:12:25 2019-08-07
243709 MainScreenAppear 5849806612437486590 1565212439 246 2019-08-07 21:13:59 2019-08-07
243710 MainScreenAppear 5746969938801999050 1565212483 246 2019-08-07 21:14:43 2019-08-07
243711 MainScreenAppear 5746969938801999050 1565212498 246 2019-08-07 21:14:58 2019-08-07
243712 OffersScreenAppear 5746969938801999050 1565212517 246 2019-08-07 21:15:17 2019-08-07

240887 rows × 6 columns

NB Убрали 1,16% событий и 0,23% уникальных пользователей от первоначального количества - что не повлияет на корректные результаты анализа эксперимента.

Убедимся, что в новом датафрейме есть пользователи из всех трех групп

In [12]:
plt.figure(figsize=(15, 10))
ax = sns.countplot(y=data_loc['date'], hue='group', data=data_loc)
ax.set_title('Количество различных событий в зависимости от времени в разрезе групп')
plt.show()
In [13]:
groups_user= data_loc.groupby('group').agg({'user_id': ['count', 'nunique']})
groups_user
Out[13]:
user_id
count nunique
group
246 79302 2484
247 77022 2513
248 84563 2537

"Путь пользователя": от первой страницы до покупки¶

Проанализируем, какие события есть в логах, как часто они встречаются, так же отсортируем события по частоте

In [14]:
event_name_log= (data_loc.groupby('event_name')['user_id']
                                .agg(['count'])
                                .rename(columns={'count':'event_count'})
                                .sort_values(by='event_count', ascending=False)
                                .reset_index())
event_name_log
Out[14]:
event_name event_count
0 MainScreenAppear 117328
1 OffersScreenAppear 46333
2 CartScreenAppear 42303
3 PaymentScreenSuccessful 33918
4 Tutorial 1005

Промежуточный вывод

  1. Чаще всего происходило событие MainScreenAppear (посещение стартового экрана программы) - 117328 раз.
  2. Далее по частоте посещений - OffersScreenAppear (посещение карточки товара) - 46333 раза
  3. Далее - CartScreenAppear - добавление товара в корзину магазина - 42303 раза
  4. Далее PaymentScreenSuccessful - оплата товара из корзины - 33918 раз.
  5. Реже всех посещалась страница Tutorial - справка по функциям программы - 1005 раз

Отсортируем события по числу пользователей, определим долю пользователей, которые хоть раз совершали событие

In [15]:
user_id_log =(data_loc.groupby('event_name')['user_id']
                                .agg(['nunique'])
                                .rename(columns={'nunique':'event_user'})
                                .sort_values(by='event_user', ascending=False)
                                .reset_index())
user_id_log['prop_user']= round(user_id_log['event_user']/ data_loc['user_id'].nunique()*100,2)
user_id_log
Out[15]:
event_name event_user prop_user
0 MainScreenAppear 7419 98.47
1 OffersScreenAppear 4593 60.96
2 CartScreenAppear 3734 49.56
3 PaymentScreenSuccessful 3539 46.97
4 Tutorial 840 11.15

Для удобства работы, сгруппируем данные по событиям и группам:

In [16]:
groups_user= data_loc.groupby('group').agg({'user_id': 'nunique'})
groups_user = pd.concat([groups_user], ignore_index=True)

groups_user #вспомогательная таблица, нужна для того, чтобы понимать сколько пользователей находится в каждой группе
Out[16]:
user_id
0 2484
1 2513
2 2537
In [17]:
user_id_log24=user_id_log
groups=[246, 247, 248]
user_id_groups =[]
i=0
for group in groups:
    user_id_groups = data_loc[data_loc['group']==group]
    user_id_log_groups =(user_id_groups.groupby('event_name')['user_id']
                                .agg(['nunique'])
                                .rename(columns={'nunique':'event_user'})
                                .sort_values(by='event_user', ascending=False)
                                .reset_index())
    user_id_log_groups['prop_user']=round(user_id_log_groups['event_user']/ (groups_user['user_id'][i])*100,2)
    user_id_log_groups=user_id_log_groups.rename(columns={'event_user':'event_'+str(group), 'prop_user':'prop_'+str(group)})
    user_id_log24=user_id_log24.merge(user_id_log_groups, on='event_name')
    i=i+1
user_id_log24
Out[17]:
event_name event_user prop_user event_246 prop_246 event_247 prop_247 event_248 prop_248
0 MainScreenAppear 7419 98.47 2450 98.63 2476 98.53 2493 98.27
1 OffersScreenAppear 4593 60.96 1542 62.08 1520 60.49 1531 60.35
2 CartScreenAppear 3734 49.56 1266 50.97 1238 49.26 1230 48.48
3 PaymentScreenSuccessful 3539 46.97 1200 48.31 1158 46.08 1181 46.55
4 Tutorial 840 11.15 278 11.19 283 11.26 279 11.00

В полученной таблице

  • event_user и event_24* - это количество уникальных пользователей;
  • prop_user и prop_24* - это доля пользователей от количества уникальных пользователей, совершивших действие.

Рассмотрим получившуюся воронку, для того чтобы наглядно увидеть последовательность действий, совершаемых пользователем.

NB К странице Tutorial - помощи, для совершения покупки - незначительное количество обращений пользователей, поэтому рассмотрим события далее без анализа действия Tutorial.

Сначала по общему количеству пользователей:

In [18]:
user_id_log24=user_id_log24[user_id_log24['event_name'] != 'Tutorial' ] #убираем Tutorial
fig=go.Figure()
fig.add_trace(go.Funnel(
                x=user_id_log24['prop_user'], 
                y=user_id_log24['event_name']))
fig.update_layout(title={'text': "Воронка событий"})
fig.show()

Исходя из полученных данных, можно предположить, что события проходят в следующем порядке: MainScreenAppear -> OffersScreenAppear -> CartScreenAppear -> PaymentScreenSuccessful

Действительно, пользователь сначал заходит на главную страницу -> рассматривает объявление -> кладет его в корзину -> оплачивает сформированный заказ.

NB. Если исходить из того, как определяется последовательность действий, то разумно предположить, что все уникальные посетители должны начинать работу с посещения главной страницы. Однако мы видим, что таких посетителей 98,47%. Примерно 1,5% посетителей попадают в приложение не через стартовую страницу.

Возможные варианты таких действий пользователей:

  • возможно, что сразу на карточку можно попасть, через рекламный банер, например, из письма, или кликнув на сайте;
  • или часть товаров уже лежало в корзине, т.е. добавлены были вне временного лога.
  • пользователь получает пуш-упоминание (?) о том, что товары лежат в корзине, и так же сразу переходит на страницу корзины, минуя главную страницу;
  • возможно сбой при записи временного лога, например, при установке обновления. Вначале анализа мы откинули первую неделю исследования, т.к. данные в ней были представленны некорректно. Хотя, логично было бы предположить, что неполные данные должны содержаться в датах последней недели исследования.

Первое, второе и третье предположение - к маркетинговому отделу, четвертое - к разработчикам.

Поскольку количество таких "аномальных" посещений не превышает 1,5%, то можем ими пренебречь в рамках проводимого исследования

Рассмотрим, как порядок событий происходит в разбивке по группам

In [19]:
groups=[246, 247, 248]

fig=go.Figure()
for group in groups:
    fig.add_trace(go.Funnel(
                name = str(group),
                x=user_id_log24['prop_'+str(group)], 
                y=user_id_log24['event_name']
                ))
fig.update_layout(title={'text': "Воронка событий по группам"})
fig.show()

Видим, что внутри групп последовательность выполнения действий та же: MainScreenAppear -> OffersScreenAppear -> CartScreenAppear -> PaymentScreenSuccessful

До оплаты (PaymentScreenSuccessful) от первоначального количества пользователей в группе:

  • 246 - 48,31% пользователей;
  • 247 - 46,08% пользователей;
  • 248 - 46,55% пользователей.

В среднем от общего количества пользователей доходит 46,97% полльзователей.

NB И так же, как и в целом для всего исследования, количество уникальных пользователей в первом действии не равно 100% количеству уникальных пользователей внутри группы!

Промежуточный вывод

  1. Стандартный "путь пользователя в приложении: MainScreenAppear -> OffersScreenAppear -> CartScreenAppear -> PaymentScreenSuccessful. Однако возможны и другие варианты последовательности событий (не более 1,5%)
  2. До покупки товаров доходит в разных группах от 46,08% до 48,31% от общего количества уникальных пользователей приложения.

Доля пользователей к предыдущему шагу воронки¶

Проанализируем теперь какое количество пользователей переходит к следующему шагу воронки от числа пользователей к предыдущему.

Для начала рассмотрим соотношение для общего количества

In [20]:
fig=go.Figure()
fig.add_trace(go.Funnel(
                x=user_id_log24['prop_user'], 
                y=user_id_log24['event_name'],
                textinfo = 'percent previous'))
fig.update_layout(title={'text': "Воронка событий"})
fig.show()

Больше всего теряется человек, при переходе с первого события MainScreenAppear на второе OffersScreenAppear - 38%.

Можно выдвинуть несколько версий:

  • на главной странице не присутствует тот товар, который интересует пользователя, поэтому он не переходит на карточку товара - ошибка маркетингового отдела;
  • на главной странице нет возможности сразу перейти на выбранный/маржинальный/акционный товар, т.е. техническая ошибка.

С другой стороны можно отметить, что

  • 81% пользователей после перехода на карточку товара OffersScreenAppear добавляют его в корзину CartScreenAppear
  • 95% добавленных товаров в корзину CartScreenAppear идут на оплату PaymentScreenSuccessful

Следовательно, товар интересен пользователю, и нужно уделить внимание первой странице, с которой начинается "путь пользователя". Возможно, как раз и стоит поработать с визуальным оформлением, проработать "путь пользователя" от начальной страницы до покупки

Рассмотрим отношение количества пользователей к предыдущему событию в разрезе групп

In [21]:
groups=[246, 247, 248]

fig=go.Figure()
for group in groups:
    fig.add_trace(go.Funnel(
                name = str(group),
                x=user_id_log24['prop_'+str(group)], 
                y=user_id_log24['event_name'],
                textinfo = 'percent previous'
                ))
fig.update_layout(title={'text': "Воронка событий по группам"})
fig.show()

Внутри групп наблюдаем ту же картину - резкое падение при переходе от первого события ко второму, и затем стабильная фиксация в районе 80-95% после добавления товара в корзину и его последущей оплате

Промежуточный вывод

  1. При переходе от стартовой страницы на страницу товара, происходит потеря 38% пользователей, точную причину назвать невозможно, т.к. не хватает информации.
  2. Возможно это связано как с технической частью приложения (сбой в программе), так и с некорректным наполнением стартовой страницы приложения (информационный/рекламный шум, неудобная навигация, оформление)
  3. В целом пользователи после перехода на карточку товара в 80% добавляют его в корзину, и в 94% оплачивают его. Что говорит о востребованности предлагаемого товара.

Вывод по разделу "Воронка продаж мобильного приложения¶

  1. Стандартный "путь пользователя в приложении: MainScreenAppear -> OffersScreenAppear -> CartScreenAppear -> PaymentScreenSuccessful. Однако возможны и другие варианты последовательности событий (не более 1,5%)
  2. До покупки товаров доходит в разных группах от 46,08% до 48,31% от общего количества уникальных пользователей приложения.
  3. Наибольшая просадка по конверсии пользователей происходит при переходе с первой ступени воронки на вторую. Потери составлют примерно 37-39 п.п. Причины могут быть связаны как с визуальным рядом первой страницы приложения, так и с программными недоработками. Имеющейся информации недостаточно для ответа на этот вопрос.
  4. В целом аудитория приложения очень лояльна - 80% пользователей добавляют товар в корзину после просмотра карточки товара, и 94% его оплачивают. Покупку совершают от 46,08% до 48,31% уникальных пользователей

А/А/В - эксперимент¶

A/A эксперимент¶

Еще раз представим, сколько пользователей находится в каждой экспериментальной группе

In [22]:
user_id_log24                          
Out[22]:
event_name event_user prop_user event_246 prop_246 event_247 prop_247 event_248 prop_248
0 MainScreenAppear 7419 98.47 2450 98.63 2476 98.53 2493 98.27
1 OffersScreenAppear 4593 60.96 1542 62.08 1520 60.49 1531 60.35
2 CartScreenAppear 3734 49.56 1266 50.97 1238 49.26 1230 48.48
3 PaymentScreenSuccessful 3539 46.97 1200 48.31 1158 46.08 1181 46.55

Определим, какой статистический тест будем использовать в исследовании.

  1. Выборки взяты случайным образом из генеральной совокупности
  2. Выборки независимы друг от друга.
  3. Размер выборк больше 30.
  4. Данные нормально распределены из-за центральной предельной теоремы

Следовательно, будем использовать z-тест. Причем, т.к. у нас две контрольных группы, то работаем с А/А/В - тестом.

Определим, что будет являться пололжительным результатом нашего исследования.

Напомню, что проведение исследования связано с предполагаемым изменением оформительского шрифта в приложении. Отдел продаж высказал опасение, что такое нововведение может снизить продажи товара через приложение, т.к. изменения оттолкнут пользователя. Отдел дизайна утверждает, что подобное изменение не сократит количество пользователей.

Исходя из этого критерием проверки гипотезы определим: в экспериментальной группе доля активных пользователей будет сопоставима с долей активных пользователей контрольных групп.

Поскольку изначально в поставленной задачи отсуствуют какие-либо количественные показатели изменений, то будем считать параметр alpha=0,05. Так же рассмотрим насколько различны результаты контрольных и эксперментальных групп при alpha=0,1.

Напишем функцию, позволяющую проводить проверку статистических гипотез. При проведении таких проверок скорректируем alpha=alpha/16 (поправка Бонферрони, т.к. мы проведём 4 эксперимента (246/247, 246/248, 247/248, 246+247/248 ) с 4 гипотезами

Для автоматизации проверки гипотез воспользуемся вспомогательной группой, в которой находится информация о номере группы, и количестве уникальных пользователей

In [23]:
groups_user= data_loc.groupby('group').agg({'group':'unique', 'user_id': 'nunique'})
groups_user = pd.concat([groups_user], ignore_index=True)
groups_user.group = groups_user.group.astype(int) 
groups_user.loc[ len(groups_user.index )] = [(groups_user['group'][0]+ groups_user['group'][1]),
                                             (groups_user['user_id'][0]+ groups_user['user_id'][1])]
groups_user
Out[23]:
group user_id
0 246 2484
1 247 2513
2 248 2537
3 493 4997

Напишем функцию вычисления результатов тестирования

In [24]:
#first_group - номер первой группы, 
#second_group - номер второй группы, 
#groups_user_f - количество уникальных пользователей для первой группы, берется из вспомогательной таблицы, 
#groups_user_s - количество уникальных пользователей для второй группы, берется из вспомогательной таблицы,
#alpha - alpha

def experiment_a_b(first_group, second_group, groups_user_f, groups_user_s, alpha):
    print ('Для групп {}, {}'.format(first_group, second_group))
    print('-----------------------------------')
    for i in user_id_log24.index:
        alpha = alpha/16
        # пропоция успехов в первой группе:
        p1 = user_id_log24['event_'+str(first_group)][i]/groups_user_f
        # пропорция успехов во второй группе:
        p2 = user_id_log24['event_'+str(second_group)][i]/groups_user_s
        # разница пропорций в датасетах
        difference = p1 - p2
        p_combined = ((user_id_log24['event_'+str(first_group)][i] +  user_id_log24['event_'+str(second_group)][i]) / 
                      (groups_user_f + groups_user_s))
        # считаем статистику в ст.отклонениях стандартного нормального распределения
        z_value = difference / mth.sqrt(p_combined * (1 - p_combined) * 
                                        (1/groups_user_f + 1/groups_user_s))
        # задаем стандартное нормальное распределение (среднее 0, ст.отклонение 1)
        distr = st.norm(0, 1) 
        p_value = (1 - distr.cdf(abs(z_value))) * 2
        print('{} p-значение: {}'.format(user_id_log24['event_name'][i], p_value))
        if (p_value < alpha):
            print("Отвергаем H0: между групами есть значимая разница")
        else:
            print("Не получилось отвергнуть H0, нет оснований считать группы разными")
        print('')

Выдвинем для проверки статистической значимости две гипотезы:

  • H0: между группами по итогам эксперимента нет значимой разницы
  • H1: между группами по итогам эксперимента существует значимая разница

Порогом статистической значимости установим alpha=0.05

Проверим выдвинутые гипотезы для контрольных групп 246, 247 при значении alpha=0.05

In [25]:
test_user_group=[[246,247]]
alpha=0.05


for i, pair  in enumerate(test_user_group):
    first_group=test_user_group[i][0]
    second_group=test_user_group[i][1]
    groups_user_f=int(groups_user.loc[groups_user['group']==first_group, 'user_id'])
    groups_user_s=int(groups_user.loc[groups_user['group']==second_group, 'user_id'])
    experiment_a_b(first_group, second_group, groups_user_f, groups_user_s, alpha)
Для групп 246, 247
-----------------------------------
MainScreenAppear p-значение: 0.7570597232046099
Не получилось отвергнуть H0, нет оснований считать группы разными

OffersScreenAppear p-значение: 0.2480954578522181
Не получилось отвергнуть H0, нет оснований считать группы разными

CartScreenAppear p-значение: 0.22883372237997213
Не получилось отвергнуть H0, нет оснований считать группы разными

PaymentScreenSuccessful p-значение: 0.11456679313141849
Не получилось отвергнуть H0, нет оснований считать группы разными

Промежуточный вывод По итогам исследования различий между двумя контрольными группами (А/А-тест) - невыявленно, обе группы показали одинаковые результаты конверсии при использовании приложения. Так же ранее мы подтвердили, что в каждой группе находятся уникальные пользователи. Следовательно можно признать данные группы контрольными

А/В эксперимент, alpha=0.05¶

Проведем анализ данных контрольных групп и экспериментальной при alpha=0.05

In [26]:
test_user_group=[[246,248], [247,248]]
alpha=0.05


for i, pair  in enumerate(test_user_group):
    first_group=test_user_group[i][0]
    second_group=test_user_group[i][1]
    groups_user_f=int(groups_user.loc[groups_user['group']==first_group, 'user_id'])
    groups_user_s=int(groups_user.loc[groups_user['group']==second_group, 'user_id'])
    experiment_a_b(first_group, second_group, groups_user_f, groups_user_s, alpha)
Для групп 246, 248
-----------------------------------
MainScreenAppear p-значение: 0.2949721933554552
Не получилось отвергнуть H0, нет оснований считать группы разными

OffersScreenAppear p-значение: 0.20836205402738917
Не получилось отвергнуть H0, нет оснований считать группы разными

CartScreenAppear p-значение: 0.07842923237520116
Не получилось отвергнуть H0, нет оснований считать группы разными

PaymentScreenSuccessful p-значение: 0.2122553275697796
Не получилось отвергнуть H0, нет оснований считать группы разными

Для групп 247, 248
-----------------------------------
MainScreenAppear p-значение: 0.4587053616621515
Не получилось отвергнуть H0, нет оснований считать группы разными

OffersScreenAppear p-значение: 0.9197817830592261
Не получилось отвергнуть H0, нет оснований считать группы разными

CartScreenAppear p-значение: 0.5786197879539783
Не получилось отвергнуть H0, нет оснований считать группы разными

PaymentScreenSuccessful p-значение: 0.7373415053803964
Не получилось отвергнуть H0, нет оснований считать группы разными

Ни в одном из проведенных расчетов не получилось отвергнуть H0.

Проведем анализ для сводной группы 246+247, ранее мы её обозначили как группа 493

In [27]:
user_id_log24['event_493']=user_id_log24['event_246']+user_id_log24['event_247']

test_user_group=[[493,248]]
alpha=0.05


for i, pair  in enumerate(test_user_group):
    first_group=test_user_group[i][0]
    second_group=test_user_group[i][1]
    groups_user_f=int(groups_user.loc[groups_user['group']==first_group, 'user_id'])
    groups_user_s=int(groups_user.loc[groups_user['group']==second_group, 'user_id'])
    experiment_a_b(first_group, second_group, groups_user_f, groups_user_s, alpha)
Для групп 493, 248
-----------------------------------
MainScreenAppear p-значение: 0.29424526837179577
Не получилось отвергнуть H0, нет оснований считать группы разными

OffersScreenAppear p-значение: 0.43425549655188256
Не получилось отвергнуть H0, нет оснований считать группы разными

CartScreenAppear p-значение: 0.18175875284404386
Не получилось отвергнуть H0, нет оснований считать группы разными

PaymentScreenSuccessful p-значение: 0.6004294282308704
Не получилось отвергнуть H0, нет оснований считать группы разными

Промежуточный вывод Проведенный A/B тест показал, что:

  • уровень значимости alpha=0,05. Такой уровень выбран как наиболее приемлимый.
  • всего было проведено 16 исследований;
  • с учетом поправки Бонферрони значение alpha принимаем равным alpha/16, иначе увеличивается вероятность получить групповую ошибку I рода;
  • значимых изменений между контрольными группами (246,247) и экспериментальной (248) не выявленно;
  • так же не выявленно значимых изменений между сводной группой (246+247) и экспериментальной (248)

A/B эксперимент, alpha=0.1¶

Проведем эксперимент при уровне значимости равной 0,1.

In [28]:
test_user_group=[[246,247],[246,248], [247,248], [493,248]]
alpha=0.1


for i, pair  in enumerate(test_user_group):
    first_group=test_user_group[i][0]
    second_group=test_user_group[i][1]
    groups_user_f=int(groups_user.loc[groups_user['group']==first_group, 'user_id'])
    groups_user_s=int(groups_user.loc[groups_user['group']==second_group, 'user_id'])
    experiment_a_b(first_group, second_group, groups_user_f, groups_user_s, alpha)
Для групп 246, 247
-----------------------------------
MainScreenAppear p-значение: 0.7570597232046099
Не получилось отвергнуть H0, нет оснований считать группы разными

OffersScreenAppear p-значение: 0.2480954578522181
Не получилось отвергнуть H0, нет оснований считать группы разными

CartScreenAppear p-значение: 0.22883372237997213
Не получилось отвергнуть H0, нет оснований считать группы разными

PaymentScreenSuccessful p-значение: 0.11456679313141849
Не получилось отвергнуть H0, нет оснований считать группы разными

Для групп 246, 248
-----------------------------------
MainScreenAppear p-значение: 0.2949721933554552
Не получилось отвергнуть H0, нет оснований считать группы разными

OffersScreenAppear p-значение: 0.20836205402738917
Не получилось отвергнуть H0, нет оснований считать группы разными

CartScreenAppear p-значение: 0.07842923237520116
Не получилось отвергнуть H0, нет оснований считать группы разными

PaymentScreenSuccessful p-значение: 0.2122553275697796
Не получилось отвергнуть H0, нет оснований считать группы разными

Для групп 247, 248
-----------------------------------
MainScreenAppear p-значение: 0.4587053616621515
Не получилось отвергнуть H0, нет оснований считать группы разными

OffersScreenAppear p-значение: 0.9197817830592261
Не получилось отвергнуть H0, нет оснований считать группы разными

CartScreenAppear p-значение: 0.5786197879539783
Не получилось отвергнуть H0, нет оснований считать группы разными

PaymentScreenSuccessful p-значение: 0.7373415053803964
Не получилось отвергнуть H0, нет оснований считать группы разными

Для групп 493, 248
-----------------------------------
MainScreenAppear p-значение: 0.29424526837179577
Не получилось отвергнуть H0, нет оснований считать группы разными

OffersScreenAppear p-значение: 0.43425549655188256
Не получилось отвергнуть H0, нет оснований считать группы разными

CartScreenAppear p-значение: 0.18175875284404386
Не получилось отвергнуть H0, нет оснований считать группы разными

PaymentScreenSuccessful p-значение: 0.6004294282308704
Не получилось отвергнуть H0, нет оснований считать группы разными

Промежуточный вывод С учетом поправки Бонферрони, различий в группах ни в одном из 16 проведнных экспериментов не было выявленно

Вывод по разделу "А/А/В - эксперимент"¶

  1. Были вывинуты 2 гипотезы:
  • H0: между группами по итогам эксперимента нет значимой разницы
  • H1: между группами по итогам эксперимента существует значимая разница
  1. Проведено 16 исследований с значением alpha=0.05, из них 12 - между контрольными и экспериментальной группами.
  2. Проведено 16 исследований с значением alpha=0.1, из них 12 - между контрольными и экспериментальной группами.
  3. По итогам проведенных исследований, с учетом введенной поправки Бонферрони, ни в одном из экспериментов не было обнаружена существенная разница между группами
  4. При условии alpha=0.1 в 10% случаев при проведении одного эксперимента существует возможность ошибочно отклонить Н0 при условии её верности. Поэтому используем alpha=0,05

Итоговый вывод¶

  1. Целью исследования было дать оценку проведенным визуальным изменениям мобильного приложения, используя методику А/А/В - эксперимента.

Отдел дизайна разработал новое оформление мобильного приложения (шрифт-схема), в течение двух недель с 25.07.2019 по 07.08.2019 проводилось тестирование в трех группах: контрольные группы (246, 247) с текущей версией приложения, и экспериментальной группой (248), у которой была установленна новая версия приложения с измененной шрифт-схемой. Действия пользователей записывались в лог-файл, который и был представлен для анализа.

  1. Всего в исследовании:
  • приняли участие 7341 уникальных пользователя (в гр.246 - 2484, в гр.247 - 2513 в гр.248 - 2537);
  • отражено 240887 событий;
  • в среднем на одного пользователя приходится 31,97 события за период
  1. Полученная информация в лог-файле была скорректирована. Из-за технических особенностей данные за первую неделю эксперимента неполные, поэтому рассматривался период с 01.08.2019 по 07.08.2019
  2. Предварительная обработка полученного файла показала, что в 98,47% пользователи идут по стандартному пути совершения покупки: MainScreenAppear -> OffersScreenAppear -> CartScreenAppear -> PaymentScreenSuccessful. Однако возможны и другие варианты последовательности событий (не более 1,5% от общего числа уникальных пользователей)
  3. До покупки товаров доходит в разных группах от 46,08% до 48,31% от общего количества уникальных пользователей приложения.
  4. Наибольшая просадка по конверсии пользователей происходит при переходе с первой ступени воронки на вторую. Потери составлют примерно 37-39 п.п. Причины могут быть связаны как с визуальным рядом первой страницы приложения, так и с программными недоработками. Имеющейся информации недостаточно для ответа на этот вопрос.
  5. В целом аудитория приложения очень лояльна - 80% пользователей добавляют товар в корзину после просмотра карточки товара, и 94% его оплачивают. Покупку совершают от 46,08% до 48,31% уникальных пользователей
  6. Анализ А/А/В - эксперимент проводился при уровне значимости alpha=0.05, а так же alpha=0.1
  7. В каждом случае было провдено по 16 исследований, из них 12 исследований - между контрольными и экспериментальной группой.
  8. С учетом большого количества проводимых экспериментов возникла вероятность получить ложнопозитивный результат (групповая вероятность ошибки первого рода). Поэтому к alpha была прменена поправка Бонферрони.
  9. По итогам проведенных исследований, с учетом введенной поправки Бонферрони, ни в одном из экспериментов не было обнаружена существенная разница между группами

Таким образом подтвердилась версия отдела дизайна, что изменение шрифта в приложении не приведет к оттоку пользователей.

С другой стороны возникает закономерный вопрос, что если такое изменение не принесло видимых результатов

  • увеличение числа совершающих покупку пользователелей,
  • сокращение разницы потери между переходом с главной страницы приложения на карточку товара,
  • увеличение количество пользователей оформивших заказ
  • и т.д., то есть ли смысл в таком нововедении?

Исходя из полученного анализа воронки продаж, необходимо сосредоточиться на вариантах сокращения потерь при пререходе пользователей с MainScreenAppear на OffersScreenAppear. Причиной потерь может быть как программная составляющая приложения, так и смысловое наполнение главной страницы.

In [ ]: